// SPDX-License-Identifier: GPL-2.0-or-later
/* Driver for Virtio QTBox device.
 *
 * Copyright 2022 HUAWEI TECHNOLOGIES CO., LTD.
 */

#include <linux/err.h>
#include <linux/slab.h>
#include "uapi/virtio_qtbox.h"
#include "virtio_qtbox_comm.h"

static int virtio_qtbox_eventq_fill_one(struct virtqueue *vq,
						struct virtio_qtbox_event_elem *elem)
{
	struct scatterlist sg[1];
	int ret;

	sg_init_one(sg, elem, sizeof(*elem));

	ret = virtqueue_add_inbuf(vq, sg, 1, elem, GFP_KERNEL);
	virtqueue_kick(vq);
	if (!ret)
		ret = vq->num_free;

	return ret;
}

void virtio_qtbox_eventq_drain(struct virtqueue *vq)
{
	struct virtio_qtbox *qtbox;
	struct virtio_qtbox_event_elem *elem;

	if (vq == NULL)
		return;

	qtbox = vq->vdev->priv;

	mutex_lock(&qtbox->eventq_mutex);

	while ((elem = virtqueue_detach_unused_buf(vq)))
		kfree(elem);

	mutex_unlock(&qtbox->eventq_mutex);
}

uint32_t virtio_qtbox_eventq_fill(struct virtqueue *vq)
{
	struct virtio_qtbox *qtbox;
	struct virtio_qtbox_event_elem *elem;
	uint32_t nr_added = 0;
	int ret;

	if (vq == NULL)
		return 0;

	qtbox = vq->vdev->priv;

	mutex_lock(&qtbox->eventq_mutex);

	do {
		elem = kzalloc(sizeof(*elem), GFP_KERNEL);
		if (!elem)
			break;

		ret = virtio_qtbox_eventq_fill_one(vq, elem);
		if (ret < 0) {
			kfree(elem);
			break;
		}

		nr_added++;
	} while (ret > 0);

	mutex_unlock(&qtbox->eventq_mutex);

	dev_info(&qtbox->vdev->dev, "add %u elements into eventq\n", nr_added);
	return nr_added;
}

static void virtio_qtbox_handle_event_message(struct virtio_qtbox *qtbox,
						struct virtio_qtbox_event_elem *elem)
{
	uint32_t op_code = le32_to_cpu(elem->op_code);
	uint32_t event = le32_to_cpu(elem->event);

	switch (op_code) {
		case VIRTIO_QTBOX_SANDBOX_VM_SERVICE:
			sandbox_handle_event(qtbox, event);
			break;
		default:
			dev_err(&qtbox->vdev->dev, "unknown event type: %u\n", op_code);
			break;
	}
}

void virtio_qtbox_event_work_handler(struct work_struct *work)
{
	struct virtio_qtbox_event_elem *elem;
	struct virtio_qtbox *qtbox;
	unsigned int len;
	int ret;

	if (work == NULL)
		return;

	qtbox = container_of(work, struct virtio_qtbox, notify_work);

	mutex_lock(&qtbox->eventq_mutex);

	while ((elem = virtqueue_get_buf(qtbox->eventq, &len))) {
		mutex_unlock(&qtbox->eventq_mutex);
		virtio_qtbox_handle_event_message(qtbox, elem);
		mutex_lock(&qtbox->eventq_mutex);
		ret = virtio_qtbox_eventq_fill_one(qtbox->eventq, elem);
		if (ret < 0) {
			dev_warn(&qtbox->vdev->dev,
				 "failed to add one elements into eventq, ret=%d\n", ret);
			kfree(elem);
		}
	}

	mutex_unlock(&qtbox->eventq_mutex);
}

void virtio_qtbox_event_cb(struct virtqueue *vq)
{
	struct virtio_qtbox *qtbox = vq->vdev->priv;

	queue_work(qtbox->event_wq, &qtbox->notify_work);
}
